package com.livemeter;

import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.jivesoftware.smack.AccountManager;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManagerListener;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterListener;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;

import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;

@SuppressWarnings("serial")
class LIM_PollEvent extends EventObject {
	public Poll poll;
	public LIM_PollEvent(Poll p,Object source) {
		super(source);
		poll = p;
	}
}

@SuppressWarnings("serial")
class LIM_MeterEvent extends EventObject {
	public Meter meter;
	public LIM_MeterEvent(Meter m,Object source)
	{
		super(source);
		meter = m;
	}
}

@SuppressWarnings("serial")
class LIM_MeterValueEvent extends LIM_MeterEvent {
	public LIM_MeterValueEvent(Meter m,MeterValue v,Object source)
	{
		super(m,source);
		value = v;
	}
	public MeterValue value;
}

@SuppressWarnings("serial")
class LIM_TopicStampEvent extends EventObject {
	public LIM_TopicStampEvent(Topic t,Object source)
	{
		super(source);
		topic = t;
	}
	public Topic topic;
}

class LIM_GroupSetup {
	public int meterValueLifeTime;
	public int voteUpdateInterval;
	public boolean chatEnabled;
	public boolean pollSubmission;
	public boolean questionsEnabled;
}

interface LIM_Event_Listener extends EventListener {
	public void meterOpened(LIM_MeterEvent evt);
	public void meterClosed(LIM_MeterEvent evt);
	public void meterValueChanged(LIM_MeterValueEvent evt);
	public void pollOpened(LIM_PollEvent evt);
	public void pollUpdated(LIM_PollEvent evt);
	public void pollClosed(LIM_PollEvent evt);
	public void topicStamp(LIM_TopicStampEvent evt);
	public void questionsChanged();
	public void setupTimeout();
	public void tick();
}

public class LIM_Engine extends Service
{
    // Binder given to clients
    private final IBinder mBinder = new LocalBinder();
    public class LocalBinder extends Binder {LIM_Engine getService() { return LIM_Engine.this;}}
	@Override
	public IBinder onBind(Intent intent) {return mBinder;}

	// singleton instance
	private static LIM_Engine m_instance = null;
	public static LIM_Engine getInstance() {return m_instance;}

	private static Gson gson = new Gson();

	SQLiteDatabase historyDB = null;
	public static boolean historyActive = false;

	@Override
	public void onCreate() {
		super.onCreate();
		if (historyActive)
		{
			historyDB = openOrCreateDatabase("LIM_History.sqlite",SQLiteDatabase.CREATE_IF_NECESSARY, null);
			// initialize DB
			//try {historyDB.execSQL("DROP TABLE groups;");} catch (Throwable e) {}
			//try {historyDB.execSQL("DROP TABLE events;");} catch (Throwable e) {}
			// sqlite integer primary key automatically is auto increment
			historyDB.execSQL("CREATE TABLE IF NOT EXISTS groups (id INTEGER PRIMARY KEY,groupUser TEXT UNICODE);");
			historyDB.execSQL("CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY,groupID INT UNSIGNED NOT NULL,type TEXT UNICODE,json TEXT UNICODE,time TIMESTAMP DEFAULT CURRENT_TIMESTAMP);");
		}
		Log.i("LIM", "LIM_ENGINE started");
    	m_instance = this;
	}

	@Override
	public void onDestroy() {
		Log.i("LIM", "LIM_ENGINE destroyed");
		m_instance = null;
		if (historyActive)
		{
			historyDB.close();
		}
		reset();
		super.onDestroy();
	}
	
	// XMPP Connection
	private XMPPConnection m_connection = null;
	
	public void reset()
	{
		Log.d("LIM-Engine","reset called!");
		m_engineMode = 0;
		if (m_connection != null) {m_connection.disconnect();m_connection = null;}
		if (m_masterConnection != null) {m_masterConnection.disconnect();m_masterConnection = null;}
		// clear timers!
		m_clientSetupTimeOutHandler.removeCallbacks(m_clientSetupTimeOutTask);
		m_clientValueTimerHandler.removeCallbacks(m_clientValueTimerTask);
		m_MasterValueTimerHandler.removeCallbacks(m_masterValueTimerTask);
	}

	// Connect to XMPP Server
	public boolean connect(String serverHost)
	{
		//ConnectionConfiguration connConfig =
		//	new ConnectionConfiguration(host, Integer.parseInt(port), service);
		SASLAuthentication.supportSASLMechanism("PLAIN", 0);
		m_connection = new XMPPConnection(serverHost);
		// Try to connect
		try {
			m_connection.connect();
			Log.i("LIM-XMPP", "Connected to " + m_connection.getHost());
		} catch (XMPPException ex) {
			showError("Failed to connect!",serverHost+ " "+ex.toString());
			m_connection = null;
			return false;
		}
		return true;
	}
	private int loginOrCreate(XMPPConnection con,String user,String pwd,String email,String name)
	{
		// Try to login
		try {
			con.login(user, pwd);
			Log.i("LIM-XMPP", "Logged in as " + con.getUser());
			return 1;

		} catch (XMPPException ex) {
			// Try to create user!
			AccountManager am = con.getAccountManager();
			if (!am.supportsAccountCreation())
			{
				showError("Incompatible server","Server does not support account creation!");
				return -1;
			}
			try {
				//Log.i("LIM-XMPP", "Create Instructions: " + am.getAccountInstructions());
				Map<String,String> attributes = new HashMap<String, String>();
				attributes.put("email", email);
				attributes.put("name", name);
				am.createAccount(user, pwd, attributes);
				Log.i("LIM-XMPP", "Created user " + user);
			} catch (XMPPException ex1) {
				showError("Invalid password?","User: "+user+"! "+ex1.toString());
				return -2;
			}
			// Before login: reconnect
			con.disconnect();
			try {
				con.connect();
				con.login(user, pwd);
				Log.i("LIM-XMPP", "Logged in as " + con.getUser());
				return 2;
			} catch (XMPPException ex1) {
				showError("Error using newly created user!","User: "+user+"! "+ex1.toString());
				return -3;
			}
		}
	}

	public boolean login(String user,String pwd)
	{
		return loginOrCreate(m_connection,user,pwd,user,user) > 0;
	}

	private byte m_engineMode = 0;
	final static private byte engineModeMaster = 1; 
	final static private byte engineModeClient = 2; 
	final static private byte engineModeHistory = 3;
	
	public boolean isMaster() {return m_engineMode == engineModeMaster;}
	public boolean isClient() {return m_engineMode == engineModeClient;}
	public boolean isHistory() {return m_engineMode == engineModeHistory;}

	protected String getServerUser(String user)
	{
		String srv = m_connection.getUser();
		srv = srv.substring(srv.indexOf("@"), srv.indexOf("/"));
		return user+srv;
	}
	
	private String m_clientGroupUser = "";
	
	protected String getGroupUser()
	{
		if (isMaster())
		{
			String temp = m_masterConnection.getUser();
			return temp.substring(0, temp.indexOf("/"));
		} else {
			return m_clientGroupUser;
		}
	}

	// CLIENT-Server protocol constants
	private final String MSG_CLIENT_SETUP_REQUEST = "CRSET";
	private final String MSG_CLIENT_METER_VALUE = "CMVAL";
	private final String MSG_CLIENT_POLL_VOTE = "CPVOT";
	private final String MSG_CLIENT_QUESTION = "CLQUE";
	private final String MSG_CLIENT_QUESTION_VOTE = "CLQVO";

	private final String MSG_MASTER_SETUP = "SETUP";
	private final String MSG_MASTER_METER_VALUE = "MMVAL";
	private final String MSG_MASTER_POLL_OPEN = "MPOPN";
	private final String MSG_MASTER_POLL_CLOSE = "MPCLS";
	private final String MSG_MASTER_POLL_VALUES = "MPVAL";
	private final String MSG_MASTER_TOPIC_STAMP = "MTOPI";
	private final String MSG_MASTER_QUESTIONS_LIST = "MMQLS";

	// ********************************
	// ********************************
	// ****** Client-Stuff ************
	// ********************************
	// ********************************

	Chat m_clientServerChat = null;
	public boolean client_JoinGroup(String groupName,String pwd)
	{
		m_engineMode = engineModeClient;
		// Try to add group name to buddy list
		m_clientGroupUser = getServerUser(groupName);
		Roster roster = m_connection.getRoster();
		try {
			if (!roster.contains(m_clientGroupUser))
			{
				roster.createEntry(m_clientGroupUser, groupName, new String[] {"Groups"} );
				Log.i("LIM-XMPP", "Added group to roster " + m_clientGroupUser);
			}
		} catch (XMPPException e) {
			showError("Error handling roster","Adding entry for: "+m_clientGroupUser+"!\n"+e.toString());
			return false;
		}
		Presence subscribe = new Presence(Presence.Type.subscribe);
		subscribe.setTo(m_clientGroupUser);
		m_connection.sendPacket(subscribe);
		
		// Add roster Listener for value updates from the master
		roster.removeRosterListener(m_clientRosterListener);
		roster.addRosterListener(m_clientRosterListener);
		// Open a chat with the master to send own values in regular intervals
		m_clientServerChat = m_connection.getChatManager().createChat(m_clientGroupUser, new MessageListener() {
		    public void processMessage(Chat chat, Message message) {
		    	LIM_Engine.this.client_onIncomingMessage(chat,message);
		    }
		});
		Log.i("LIM-XMPP", "Joined group! Sending setup request!");
		// Send Setup Request Message to master
		try {
			m_clientSetupRecieved = false;
			m_clientServerChat.sendMessage(LIM_JSON_Message.encode(MSG_CLIENT_SETUP_REQUEST,""));
			// Start setupTimeOutTimer
			m_clientSetupTimeOutHandler.removeCallbacks(m_clientSetupTimeOutTask);
			m_clientSetupTimeOutHandler.postDelayed(m_clientSetupTimeOutTask,5000);
		} catch (XMPPException e) {
			showError("Error sending test message to master",e.toString());
		}
		m_groupPwd = pwd;
		m_groupName = groupName;
		return true;
	}
	
	Handler m_clientSetupTimeOutHandler = new Handler();
	private Runnable m_clientSetupTimeOutTask = new Runnable() {
        public void run() 
        {
        	if (m_clientSetupRecieved == false) 
        	{
				Vector<LIM_Event_Listener> listeners = (Vector<LIM_Event_Listener>)m_listeners.clone();
        		for (LIM_Event_Listener listener : listeners)
				{
					listener.setupTimeout();
				}
        	}
        }
        };

	RosterListener m_clientRosterListener = new RosterListener() {
		public void presenceChanged(Presence p)
		{
			LIM_Engine.this.client_onIncomingPresence(p);
		}
		@Override
		public void entriesAdded(Collection<String> arg0) {}
		@Override
		public void entriesDeleted(Collection<String> arg0) {}
		@Override
		public void entriesUpdated(Collection<String> arg0) {};
    };

    boolean m_clientSetupRecieved = false;
    
    protected void client_HandleMessage(LIM_JSON_Message msg,long now,boolean fromHistory)
    {
    	if (now == 0)
    	{
    		now = Calendar.getInstance().getTime().getTime();
    	}
		// Setup Answer
    	// (!m_clientSetupRecieved) || ((fromHistory))
		if ((msg.type.equalsIgnoreCase(MSG_MASTER_SETUP)))
		{
			Log.i("LIM-XMPP","Handling Setup: "+msg.json_body);
			JsonParser parser = new JsonParser();
			JsonArray array = parser.parse(msg.json_body).getAsJsonArray();
			// Meters
			Type MeterListType = new TypeToken<ArrayList<Meter>>(){}.getType();
			ArrayList<Meter> meters = gson.fromJson(array.get(0), MeterListType);
			for (Meter meter : meters)
			{
				Meter m2 = Meter.getMeter(meter.getTitle());
				if (m2 == null)
				{
					Meter.addMeter(meter);
					m2 = meter;
				} else {
					Log.i("LIM-XMPP", "Meter does already exist! "+meter.getTitle());
				}
				// if the meter is not yet active => activate it!
				if (!m2.isActive())
					m2.setActive(true);
				//// if the meter is not yet shown => show it!
				if (!m2.hasBeenShown())
				{
					// Need to copy the listeners, because in that event handler
					// the history stuff will insert another listener  
					@SuppressWarnings("unchecked")
					Vector<LIM_Event_Listener> listeners = (Vector<LIM_Event_Listener>)m_listeners.clone();
					LIM_MeterEvent me = new LIM_MeterEvent(meter,this);
					for (LIM_Event_Listener listener : listeners)
					{
						listener.meterOpened(me);
					}
				}
			}
			// Polls
			Type PollListType = new TypeToken<ArrayList<Poll>>(){}.getType();
			ArrayList<Poll> polls = gson.fromJson(array.get(1), PollListType);
			for (Poll poll : polls)
			{
				client_openPoll(poll);
			}
			// Questions
			Type QuestionListType = new TypeToken<ArrayList<Question>>(){}.getType();
			ArrayList<Question> questions = gson.fromJson(array.get(2), QuestionListType);
			Question.updateQuestions(questions);
			// Group Setup
			LIM_GroupSetup groupsetup = gson.fromJson(array.get(3), LIM_GroupSetup.class);
			client_StartValueTimer(groupsetup.voteUpdateInterval);
			m_clientValueLifetime = groupsetup.meterValueLifeTime;

			m_clientSetupRecieved = meters.size() > 0;
			// Store event in local history
			if ((!fromHistory) && (m_clientSetupRecieved))
				history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
			return;
		}
		// Master Meter Value
		if (msg.type.equalsIgnoreCase(MSG_MASTER_METER_VALUE))
		{
			Type MasterValuesType = new TypeToken<HashMap<String,MeterValue>>(){}.getType();
			HashMap<String,MeterValue> masterValues = gson.fromJson(msg.json_body, MasterValuesType);
			for (Map.Entry<String, MeterValue> entry : masterValues.entrySet())
			{
				MeterValue mv = entry.getValue();
				String meterTitle = entry.getKey();
				// Set TimeStamp to local time!
				mv.setTime(now);
				Meter m = Meter.getMeter(meterTitle);
				if (m != null)
				{
					// Set local value!
					int localValue = -1;
					if (m_clientCurrentValue.containsKey(meterTitle))
					{
						ClientMeterValueChanged mvc = m_clientCurrentValue.get(meterTitle);
						if (now - mvc.timeOfValue < m_clientValueLifetime)
						{
							localValue = mvc.value;
						}
					}
					mv.setValueLocalMeter(localValue);

					//if (!fromHistory) {
					LIM_MeterValueEvent evt = new LIM_MeterValueEvent(m,mv,this);
					m_clientCurrentGroupValues.put(meterTitle, evt);
					for (LIM_Event_Listener listener : m_listeners)
					{
							listener.meterValueChanged(evt);
					}
				} else {
					showError("Value update for unknown meter!",meterTitle);
				}
			}
			return;
		}
		// Poll Open
		if (msg.type.equalsIgnoreCase(MSG_MASTER_POLL_OPEN))
		{
			Poll poll = gson.fromJson(msg.json_body, Poll.class);
			client_openPoll(poll);
			// Story event in local history
			if (!fromHistory)
				history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
			return;
		}
		// Poll Values
		if (msg.type.equalsIgnoreCase(MSG_MASTER_POLL_VALUES))
		{
			// Update Poll option votes
			Poll poll = gson.fromJson(msg.json_body, Poll.class);
			Poll newPoll = Poll.getPoll(poll.getTitle());
			if (newPoll == null)
			{
				showError("Value update for unknown poll!",poll.getTitle());
				return;
			}
			// Show Poll title
			Log.i("LIM-XMPP", "Poll updated: "+poll.getTitle());
			ArrayList<PollOption> pos = newPoll.getOptions(true);
			pos.clear();
			pos.addAll(poll.getOptions(false));
			// Notify listeners - Fire Poll Update Event
			LIM_PollEvent evt = new LIM_PollEvent(newPoll,this);
			for (LIM_Event_Listener listener : m_listeners)
			{
				listener.pollUpdated(evt);
			}
			// Story event in local history
			if (!fromHistory)
				history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
			return;
		}
		// Poll close
		if (msg.type.equalsIgnoreCase(MSG_MASTER_POLL_CLOSE))
		{
			String pollTitle = gson.fromJson(msg.json_body, String.class);
			Log.i("LIM-XMPP", "Closing Poll: "+pollTitle);
			Poll poll2 = Poll.getPoll(pollTitle);
			if (poll2 != null)
			{
				poll2.setActive(false);
				// Fire Poll Close Event
				LIM_PollEvent evt = new LIM_PollEvent(poll2,this);
				for (LIM_Event_Listener listener : m_listeners)
				{
					listener.pollClosed(evt);
				}
			}
			// Store event in local history
			if (!fromHistory)
				history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
			return;
		}
		// TOPIC Stamp
		if (msg.type.equalsIgnoreCase(MSG_MASTER_TOPIC_STAMP))
		{
			Topic topic = gson.fromJson(msg.json_body, Topic.class);
			topic.setTime(now);
			Log.i("LIM-XMPP", "Topic Stamp: "+topic.getTopicText());
			LIM_TopicStampEvent evt = new LIM_TopicStampEvent(topic,this);
			for (LIM_Event_Listener listener : m_listeners)
			{
				listener.topicStamp(evt);
			}
			// Story event in local history
			if (!fromHistory)
				history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
			return;
		}
		// Question List
		if (msg.type.equalsIgnoreCase(MSG_MASTER_QUESTIONS_LIST))
		{
			Type QuestionListType = new TypeToken<ArrayList<Question>>(){}.getType();
			ArrayList<Question> questions = gson.fromJson(msg.json_body, QuestionListType);
			Log.i("LIM-XMPP", "QuestionList recieved: "+String.valueOf(questions.size()));
			Question.updateQuestions(questions);
			// emit questions changed
			Log.i("LIM", "Emitting questionsChanged: "+String.valueOf(m_listeners.size()));
			for (LIM_Event_Listener listener : m_listeners)
			{
				listener.questionsChanged();
			}
			return;
		}
    }
    
	protected void client_onIncomingMessage(Chat chat,Message message)
	{
		LIM_JSON_Message msg = LIM_JSON_Message.decode(message.getBody());
		Log.i("LIM-XMPP", "Recieved message from master: ("+msg.type+") "+msg.json_body);
		client_HandleMessage(msg,0,false);
	}

	private Handler m_clientValueTimerHandler = new Handler();
	private int m_clientValueTimerInterval = 5000;

	protected void client_StartValueTimer(int interval)
	{
		m_clientValueTimerInterval = interval;
		m_clientValueTimerHandler.removeCallbacks(m_clientValueTimerTask);
		m_clientValueTimerHandler.postDelayed(m_clientValueTimerTask, m_clientValueTimerInterval);
	}
	private Runnable m_clientValueTimerTask = new Runnable() {
		public void run() {
			client_SendCurrentValue();
			client_FireMeterValueUpdate();
			// doTick
			for (LIM_Event_Listener listener : m_listeners) {listener.tick();}
			m_clientValueTimerHandler.postDelayed(this,m_clientValueTimerInterval);
		}
	};

	class ClientMeterValueChanged {
		ClientMeterValueChanged(String t,int v,long ts,boolean c) {meterTitle = t;value = v;changed = c;timeOfValue = ts;}
		public String meterTitle;
		public int value;
		public boolean changed;
		public long timeOfValue;
	};

	HashMap<String,ClientMeterValueChanged> m_clientCurrentValue = new HashMap<String,ClientMeterValueChanged>();
	boolean m_clientValueChanged = false;

	protected void client_SendCurrentValue()
	{
		if (!m_clientValueChanged) return;
		for (ClientMeterValueChanged mvc : m_clientCurrentValue.values())
		{
			if (mvc.changed)
			{
				try {
					m_clientServerChat.sendMessage(LIM_JSON_Message.encode(MSG_CLIENT_METER_VALUE,gson.toJson(mvc)));
				} catch (XMPPException e) {
					showError("Error sending meter value to master",e.toString());
				}
				mvc.changed = false;
			}
		}
		m_clientValueChanged = false;
	}

	public void client_MeterSetValue(String meterTitle,int localValue)
	{
		long now = Calendar.getInstance().getTime().getTime();
		if (!m_clientCurrentValue.containsKey(meterTitle))
			m_clientCurrentValue.put(meterTitle, new ClientMeterValueChanged(meterTitle,-1,now,true));
		ClientMeterValueChanged mvc = m_clientCurrentValue.get(meterTitle);
		if (mvc.value != localValue)
		{
			mvc.value = localValue;
			mvc.timeOfValue = now;
			mvc.changed = true;
			m_clientValueChanged = true;
		}
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void client_PollVoteFor(Poll poll,int optionIndex)
	{
		Collection collection = new ArrayList();
		collection.add(poll.getTitle());
		collection.add(optionIndex);
		// Send message to master
		try {
			m_clientServerChat.sendMessage(LIM_JSON_Message.encode(MSG_CLIENT_POLL_VOTE,gson.toJson(collection)));
		} catch (XMPPException e) {
			showError("Error sending vote to master",e.toString());
		}
	}
	
	public void client_QuestionVote(Question q)
	{
		try {
			m_clientServerChat.sendMessage(LIM_JSON_Message.encode(MSG_CLIENT_QUESTION_VOTE,gson.toJson(q)));
		} catch (XMPPException e) {
			showError("Error sending vote to master",e.toString());
		}
	}
	
	public void client_AskQuestion(String questionStr)
	{
		try {
			Question q = new Question();
			q.question = questionStr;
			q.time = Calendar.getInstance().getTime().getTime();
			m_clientServerChat.sendMessage(LIM_JSON_Message.encode(MSG_CLIENT_QUESTION,gson.toJson(q)));
		} catch (XMPPException e) {
			showError("Error sending question to master",e.toString());
		}
	}

	class MeterValueTitled
	{
		MeterValueTitled(String mt,MeterValue v) {meterTitle = mt;value = v;}
		String meterTitle;
		MeterValue value;
	}

	HashMap<String,LIM_MeterValueEvent> m_clientCurrentGroupValues = new HashMap<String,LIM_MeterValueEvent>();
	
	// Poll is a new poll, send over from the master
	// Checks if a poll with the same title is present
	private void client_openPoll(Poll poll)
	{
		Poll newPoll = Poll.getPoll(poll.getTitle());
		if (newPoll == null)
			newPoll = Poll.addNewPoll(poll.getTitle(), poll.getQuestion(), poll.isPermanent());
		if (!newPoll.isActive())
		{
			newPoll.setActive(isClient());
			// Show Poll title
			Log.i("LIM-XMPP", "Poll opened: "+poll.getTitle());
			ArrayList<PollOption> pos = newPoll.getOptions(true);
			pos.clear();
			pos.addAll(poll.getOptions(false));
			// Fire Poll Open Event
			LIM_PollEvent evt = new LIM_PollEvent(newPoll,this);
			for (LIM_Event_Listener listener : m_listeners)
			{
				listener.pollOpened(evt);
			}
		} else {
			Log.i("LIM-XMPP", "Poll was already opened: "+newPoll.getTitle());
		}
	}

	protected void client_onIncomingPresence(Presence p)
	{
		Log.i("LIM-XMPP", "Presence changed: "+p.getFrom()+" "+p.getStatus());
		LIM_JSON_Message msg = LIM_JSON_Message.decode(p.getStatus());
		client_HandleMessage(msg,0,false);
	}

	MeterValue m_clientLastUpdatedValue = null; 
	
	protected void client_FireMeterValueUpdate()
	{
		/*long now = Calendar.getInstance().getTime().getTime();
		for (LIM_MeterValueEvent evt : m_clientCurrentGroupValues.values())
		{
			// Set TimeStamp to local time!
			evt.value.setTime(now);
			// Get local value
			byte localValue = 0;
			// if there is a current meter value and its not too old
			if (m_clientCurrentValue.containsKey(evt.meter.getTitle()))
			{
				ClientMeterValueChanged mvc = m_clientCurrentValue.get(evt.meter.getTitle());
				if (now - mvc.timeOfValue < m_clientValueLifetime)
					localValue = mvc.value;
			}
			evt.value.setValueLocalMeter(localValue);

			// Store value in history_DB ?
			if ((m_clientLastUpdatedValue == null) || (!m_clientLastUpdatedValue.isEqualTo(evt.value, true)))
			{
				HashMap<String,MeterValue> values = new HashMap<String,MeterValue>();
				values.put(evt.meter.getTitle(), evt.value);
				history_StoreEvent(getGroupUser(),MSG_MASTER_METER_VALUE,gson.toJson(values));
				m_clientLastUpdatedValue = evt.value;
			}
		}*/
	}

	private static long m_clientValueLifetime = 300000;
	
	// ********************************
	// ********************************
	// ****** Master-Stuff ************
	// ********************************
	// ********************************

	private String m_groupName = "";
	private String m_groupPwd = "";
	
	private XMPPConnection m_masterConnection = null;
	public boolean master_CreateGroup(String groupName,String pwd)
	{
		m_engineMode = engineModeMaster;
		// create user for group
		ConnectionConfiguration config = new ConnectionConfiguration(m_connection.getHost(),m_connection.getPort(),m_connection.getServiceName());
		m_masterConnection = new XMPPConnection(config);
		try {
			m_masterConnection.connect();
		} catch (XMPPException e) {
			showError("Error connecting!","Error connecting master connection!\n"+e.toString());
			return false;
		}
		// Does the group already exist? Try to login
		int groupUserLoginResult = loginOrCreate(m_masterConnection,groupName,pwd,m_connection.getUser(),groupName);
		if (groupUserLoginResult > 0)
		{
			if (groupUserLoginResult == 1)
			{
				// Login successful => Group exists! Correct user?
				//String email = am.getAccountAttribute("email");
				//if (email.compareTo(m_connection.getUser()) != 0)
				//{	// +email+" vs "+m_connection.getUser()
				//	showError("Not your group!","Group: "+groupName+" was created by another user: "+email+"!");
				//	return false;
				//}
			}
		} else {
			showError("Error creating group!",m_lastError+"\n"+m_lastErrorLong);
			return false;
		}
		
		m_masterConnection.getChatManager().removeChatListener(m_masterClientChatListener);
		m_masterConnection.getChatManager().addChatListener(m_masterClientChatListener);

		m_groupPwd = pwd;
		m_groupName = groupName;

		master_StartValueTimer(2);
		return true;
	}
	
	public String getGroupInfo()
	{
		String res = new String();
		if (m_connection != null)
		{
			res += "Group: "+m_groupName+" Pwd: "+m_groupPwd;
			res += "\nServer: "+m_connection.getHost();
			if (isMaster())
			{
				res += "\nUsers Active: "+m_masterMeterActiveClientCount+" Total: "+m_masterMeterTotalClientCount;
			}
		}
		return res;
	}
	
	ChatManagerListener m_masterClientChatListener = new ChatManagerListener() {
		MessageListener chatMsgListener = new MessageListener() {
			@Override
		    public void processMessage(Chat chat, Message message)
			{
				LIM_Engine.this.master_OnClientMessageRecieved(chat, message);
			}
		};
		@Override
        public void chatCreated(Chat chat, boolean createdLocally)
        {
            if (createdLocally) return;
            chat.removeMessageListener(chatMsgListener);
            chat.addMessageListener(chatMsgListener);
        }
    };
    
    public void master_SetupFinished()
    {
    	LIM_JSON_Message setupMsg = master_CreateSetupMessage();
		// Store in history
		history_StoreEvent(getGroupUser(), setupMsg.type, setupMsg.json_body);
    }

	// **** CLIENT MESSAGE RECIEVED ****
	
	protected void master_OnClientMessageRecieved(Chat chat, Message message)
	{
		LIM_JSON_Message msg = LIM_JSON_Message.decode(message.getBody());
		Log.i("LIM-XMPP", "Client sent message: ("+msg.type+") "+msg.json_body);
		// Setup Request
		if (msg.type.equalsIgnoreCase(MSG_CLIENT_SETUP_REQUEST))
		{
            try {
            	LIM_JSON_Message setupMsg = master_CreateSetupMessage();
            	String setupStr = setupMsg.encode();
				Log.i("LIM-XMPP", "Sending Setup: "+setupStr);
				chat.sendMessage(setupStr);
			} catch (XMPPException e) {
				showError("Error sending master setup!","to: "+chat.getParticipant()+"\n"+e.toString());
			}
		}
		// Meter Value
		if (msg.type.equalsIgnoreCase(MSG_CLIENT_METER_VALUE))
		{
			ClientMeterValueChanged mvc = gson.fromJson(msg.json_body, ClientMeterValueChanged.class);
			if (!m_masterCurrentClientValues.containsKey(mvc.meterTitle))
				m_masterCurrentClientValues.put(mvc.meterTitle, new HashMap<String,ClientMeterValue>());
			HashMap<String,ClientMeterValue> meterClientValues = m_masterCurrentClientValues.get(mvc.meterTitle);
			// Insert this clients value
			long now = Calendar.getInstance().getTime().getTime();
			meterClientValues.put(chat.getParticipant(), new ClientMeterValue(mvc.value, now) );
			// Update group meter values
			master_UpdateGroupMeterValues(mvc.meterTitle);
		}
		// Poll Vote
		if (msg.type.equalsIgnoreCase(MSG_CLIENT_POLL_VOTE))
		{
			JsonParser parser = new JsonParser();
			JsonArray array = parser.parse(msg.json_body).getAsJsonArray();
			String pollTitle = gson.fromJson(array.get(0), String.class);
			int optionIndex = gson.fromJson(array.get(1), Integer.class);
			Poll p = Poll.getPoll(pollTitle);
			if ((p != null) && (p.isActive()))
			{
				// Store Value in per poll/client HashMap
				if (!m_masterPollVotes.containsKey(pollTitle))
					m_masterPollVotes.put(pollTitle, new MasterPollResults(p));
				MasterPollResults mpr = m_masterPollVotes.get(pollTitle);
				mpr.addVote(chat.getParticipant(), optionIndex);
				m_masterPollVotesChanged = true;
				master_FirePollChanged(p);
			} else {
				showError("Vote for an invalid/closed poll!","from: "+chat.getParticipant());
			}
		}
	}
	
	class MasterPollResults
	{
		MasterPollResults(Poll p)
		{
			poll = p;
		}
		public Poll poll;
		public boolean changed = false;
		void addVote(String user,int optionIndex)
		{
			ArrayList<PollOption> pos = poll.getOptions(true);
			if (votes.containsKey(user))
			{
				int oldValue = votes.get(user);
				try {
					pos.get(oldValue).setVotesForOption(pos.get(oldValue).getVotesForOption()-1);
				} catch (IndexOutOfBoundsException e) {
				}
			}
			try {
				pos.get(optionIndex).setVotesForOption(pos.get(optionIndex).getVotesForOption()+1);
				votes.put(user, optionIndex);
			} catch (IndexOutOfBoundsException e) {
			}
			changed = true;
		}
		private HashMap<String,Integer> votes = new HashMap<String,Integer>();
	}
	
	HashMap<String,MasterPollResults> m_masterPollVotes = new HashMap<String,MasterPollResults>();
	boolean m_masterPollVotesChanged = false;

	@SuppressWarnings("unchecked")
	protected LIM_JSON_Message master_CreateSetupMessage()
	{
		@SuppressWarnings("rawtypes")
		Collection collection = new ArrayList();
		collection.add(Meter.getActiveMeters());
		collection.add(Poll.getActivePolls());

		return new LIM_JSON_Message(MSG_MASTER_SETUP,gson.toJson(collection));
	}

    // **** METER VALUE STUFF ***
	
    class ClientMeterValue
    {
    	ClientMeterValue(int v,long t) {value = v;time = t;}
    	int value;
    	long time;
    }

	HashMap<String,HashMap<String,ClientMeterValue>> m_masterCurrentClientValues = new HashMap<String,HashMap<String,ClientMeterValue>>();

	protected int getPercentile(int p,int[] sortedValues)
	{
		int n = sortedValues.length;
		if (n == 1)
			return sortedValues[0];
		double pos = (double)p * ((double)n + 1) / 100;
		double fpos = Math.floor(pos);
		int intPos = (int) fpos;
		double dif = pos - fpos;
		if (pos < 1) {
		    return sortedValues[0];
		}
		if (pos >= n) {
		    return sortedValues[sortedValues.length - 1];
		}
		double lower = sortedValues[intPos - 1];
		double upper = sortedValues[intPos];
		return (int)(lower + dif * (upper - lower));
	}

	HashMap<String,MeterValue> m_masterGroupValues = new HashMap<String,MeterValue>();
	boolean m_masterGroupValuesChanged = false;

	private void master_setGroupValue(String meterTitle,MeterValue mv)
	{
		MeterValue mva = m_masterGroupValues.get(meterTitle);
		if ((mva == null) 
				|| (mva.getValueAverage() != mv.getValueAverage())
				|| (mva.getValueLocalMeter() != mv.getValueLocalMeter())
				|| (mva.getValuePercentile10() != mv.getValuePercentile10())
				|| (mva.getValuePercentile90() != mv.getValuePercentile90())
				//|| (!mva.getTime().equals(mv.getTime())) 
				)
		{
			m_masterGroupValues.put(meterTitle, mv);
			m_masterGroupValuesChanged = true;
		}
	}
	
	private int m_masterMeterTotalClientCount = 0;
	private int m_masterMeterActiveClientCount = 0;
	
	protected void master_UpdateGroupMeterValues(String meterTitle)
	{
		if (!m_masterCurrentClientValues.containsKey(meterTitle)) return;
		long now = Calendar.getInstance().getTime().getTime();
		// Create copy of meterClientValues
		HashMap<String,ClientMeterValue> meterClientValues = new HashMap<String,ClientMeterValue>(m_masterCurrentClientValues.get(meterTitle));
		m_masterMeterTotalClientCount = meterClientValues.size();
		// Remove all expired values
		for(Iterator<HashMap.Entry<String, ClientMeterValue>> it = meterClientValues.entrySet().iterator(); it.hasNext();)
		{
			HashMap.Entry<String, ClientMeterValue> entry = it.next();
			if (now-entry.getValue().time > m_clientValueLifetime)
			{
				it.remove();
			}
		}
		m_masterMeterActiveClientCount = meterClientValues.size();

		// Check if group is empty
		if (meterClientValues.isEmpty()) 
		{
			master_setGroupValue(meterTitle, new MeterValue(-1,-1,-1,-1,now,0));
			return;
		}
		int[] values = new int[meterClientValues.size()];
		int i = 0;
		for (ClientMeterValue cmv:meterClientValues.values())
		{
			values[i] = cmv.value;
			++i;
		}
		java.util.Arrays.sort(values);
		Log.d("LIM_XMPP", "sorted client values: "+java.util.Arrays.toString(values));
		int sum = 0;
		for (int j=0;j<values.length;++j) {sum += values[j];}
		// Read 10,50,90 Percentile
		MeterValue mv = new MeterValue(getPercentile(10,values),getPercentile(90,values),(int)(sum/values.length),(int)-1,now,values.length);
		master_setGroupValue(meterTitle,mv);
	}

	private Handler m_MasterValueTimerHandler = new Handler();
	private int m_MasterValueTimerInterval = 5000;
	
	private long m_MasterMeterLastFullUpdate = 0;

	protected void master_StartValueTimer(int interval)
	{
		m_MasterValueTimerInterval = interval*1000;
		m_MasterValueTimerHandler.removeCallbacks(m_masterValueTimerTask);
		m_MasterValueTimerHandler.postDelayed(m_masterValueTimerTask, m_MasterValueTimerInterval);
	}
	private Runnable m_masterValueTimerTask = new Runnable() {
		public void run() {
			long now = System.currentTimeMillis();
			// Every now and then do a full check for changed/expired values
			if (now - m_MasterMeterLastFullUpdate > 10000)
			{
				for (String meterTitle:m_masterCurrentClientValues.keySet())
				{
					master_UpdateGroupMeterValues(meterTitle);
				}
				m_MasterMeterLastFullUpdate = now;
			}
			master_SendGroupValues();
			master_FireMeterValueUpdate();
			master_SendPollResults();
			m_MasterValueTimerHandler.postDelayed(this,m_MasterValueTimerInterval);
		}
	};
	
	protected void master_FireMeterValueUpdate()
	{
		/*long now = Calendar.getInstance().getTime().getTime();
		for (Entry<String,MeterValue> entry : m_masterGroupValues.entrySet())
		{
			// Set TimeStamp to local time!
			MeterValue mv = new MeterValue(entry.getValue());
			mv.setTime(now);
			Meter m = Meter.getMeter(entry.getKey());
			if (m != null)
			{
				LIM_MeterValueEvent evt = new LIM_MeterValueEvent(m,mv,this); 
				for (LIM_Event_Listener listener : m_listeners)
				{
					listener.meterValueUpdate(evt);
				}
			}
		}*/
	}

	protected void master_SendGroupValues()
	{
		if (!m_masterGroupValuesChanged) return;
		if (m_masterConnection != null)
		{
			LIM_JSON_Message msg = new LIM_JSON_Message(MSG_MASTER_METER_VALUE,gson.toJson(m_masterGroupValues));
			String m = msg.encode();
			Log.i("LIM_XMPP", "Sending group values: "+m);
			Presence presence = new Presence(Presence.Type.available,m,0,Presence.Mode.available);
			m_masterConnection.sendPacket(presence);
			m_masterGroupValuesChanged = false;
			// Story event in local history
			history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
		} else {
			Log.w("LIM-Engine", "Still running, in spite of master connection disconnected!");
		}
	}

	public void master_OpenPoll(String pollTitle)
	{
		Log.i("LIM_XMPP", "openening poll: "+pollTitle);
		Poll p = Poll.getPoll(pollTitle);
		if (p == null) return;
		p.setActive(true);
		// Send Presence with poll details
		LIM_JSON_Message msg = new LIM_JSON_Message(MSG_MASTER_POLL_OPEN,gson.toJson(p));
		String m = msg.encode();
		Log.i("LIM_XMPP", "Sending poll open: "+m);
		Presence presence = new Presence(Presence.Type.available,m,0,Presence.Mode.available);
		m_masterConnection.sendPacket(presence);
		// Story event in local history
		history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
	}

	public void master_ClosePoll(String pollTitle)
	{
		Poll p = Poll.getPoll(pollTitle);
		if (p == null) return;
		p.setActive(false);
		// Send Presence with poll details
		LIM_JSON_Message msg = new LIM_JSON_Message(MSG_MASTER_POLL_CLOSE,gson.toJson(pollTitle));
		String m = msg.encode();
		Log.i("LIM_XMPP", "Sending poll close: "+m);
		Presence presence = new Presence(Presence.Type.available,m,0,Presence.Mode.available);
		m_masterConnection.sendPacket(presence);
		// Story event in local history
		history_StoreEvent(getGroupUser(),msg.type,msg.json_body);
	}
	
	private transient Topic m_masterLastTopic = null;
	public Topic master_LastTopicStamp() {return m_masterLastTopic;}
	
	public void master_SendTopicStamp(Topic t)
	{
		// Send Presence with poll details
		LIM_JSON_Message msg = new LIM_JSON_Message(MSG_MASTER_TOPIC_STAMP,gson.toJson(t.getTopicText()));
		String m = msg.encode();
		Log.i("LIM_XMPP", "Sending topic stamp: "+m);
		Presence presence = new Presence(Presence.Type.available,m,0,Presence.Mode.available);
		m_masterConnection.sendPacket(presence);
		// fire local event
		m_masterLastTopic = t;
		long now = Calendar.getInstance().getTime().getTime();
		t.setTime(now);
		// Store topic in local db
		history_StoreEvent(getGroupUser(), msg.type, msg.json_body);
		// Notify local listeners
		master_FireTopicStamp(t);
	}

	protected void master_FireTopicStamp(Topic t)
	{
		LIM_TopicStampEvent evt = new LIM_TopicStampEvent(t,this);
		for (LIM_Event_Listener listener : m_listeners)
		{
			listener.topicStamp(evt);
		}
	}
	
	protected void master_SendPollResults()
	{
		if (!m_masterPollVotesChanged) return;
		for (MasterPollResults mpr : m_masterPollVotes.values())
		{
			if (mpr.changed)
			{
				// Send Presence with poll details
				LIM_JSON_Message msg = new LIM_JSON_Message(MSG_MASTER_POLL_VALUES,gson.toJson(mpr.poll));
				String m = msg.encode();
				Log.i("LIM_XMPP", "Sending poll values: "+m);
				Presence presence = new Presence(Presence.Type.available,m,0,Presence.Mode.available);
				m_masterConnection.sendPacket(presence);
				mpr.changed = false;
				// Store in history
				history_StoreEvent(getGroupUser(), msg.type, msg.json_body);
			}
		}
		m_masterPollVotesChanged = false;
	}
	
	protected void master_FirePollChanged(Poll p)
	{
		LIM_PollEvent evt = new LIM_PollEvent(p,this); 
		for (LIM_Event_Listener listener : m_listeners)
		{
			listener.pollUpdated(evt);
		}
	}
	
	
	// ********************************
	// ********************************
	// ****** History-Stuff ***********
	// ********************************
	// ********************************
	
	public ArrayList<String> history_GetGroupNames()
	{
		ArrayList<String> result = new ArrayList<String>();
		if (historyActive)
		{
			Cursor c = historyDB.rawQuery("SELECT groupUser FROM groups;", null);
	        int userColumn = c.getColumnIndex("groupUser");
	        while (c.moveToNext()) {
	        	result.add(c.getString(userColumn));
	        }
	        c.close();
		}
        return result;
	}
	
	public void history_startHistoryMode()
	{
		m_engineMode = engineModeHistory;
	}
	
	private int history_GetGroupID(String groupUser)
	{
		//historyDB.execSQL("CREATE TABLE IF NOT EXISTS groups (id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,title TEXT UNICODE);");
		int result = -1;
		if (historyActive)
		{
			Cursor c = historyDB.rawQuery("SELECT id FROM groups WHERE groupUser='"+groupUser+"';", null);
	        int idColumn  = c.getColumnIndex("id");
	        if (c.moveToNext()) {
	            result = c.getInt(idColumn);
	        } else {
	        	// Insert group!
	        	historyDB.execSQL("INSERT INTO groups (groupUser) VALUES ('"+groupUser+"');");
	    		Cursor d = historyDB.rawQuery("SELECT id FROM groups WHERE groupUser='"+groupUser+"';", null);
	            idColumn = d.getColumnIndex("id");
	            if (d.moveToNext()) {
	                result = d.getInt(idColumn);
	            } else {
	            	result = -1;
	            }
	            d.close();
	        }
	        c.close();
		}
        return result;
	}

	// Loads and handles all events for that group!
	// Assumes that there is a proper Event Listener registered
	public void history_LoadGroup(String groupName)
	{
		if (historyActive)
		{
			int groupID = history_GetGroupID(groupName);
			Cursor c = historyDB.rawQuery("SELECT type,json,time FROM events WHERE groupID="+groupID+";", null);
	        int typeColumn  = c.getColumnIndex("type");
	        int jsonColumn  = c.getColumnIndex("json");
	        int timeColumn = c.getColumnIndex("time");
	        while (c.moveToNext()) {
	        	LIM_JSON_Message msg = new LIM_JSON_Message(c.getString(typeColumn),c.getString(jsonColumn));
	        	Log.i("LIM-History",c.getPosition()+": "+msg.type+"@"+c.getString(timeColumn)+"/"+msg.json_body);
	        	client_HandleMessage(msg,Timestamp.valueOf(c.getString(timeColumn)).getTime(),true);
	        }
	        c.close();
		}
	}
	
	public void history_StoreEvent(String groupName,String msgType,String json)
	{
		if (historyActive)
		{
			int groupID = history_GetGroupID(groupName);
			// CREATE TABLE IF NOT EXISTS events (id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
			//   groupID INT UNSIGNED NOT NULL,type TEXT UNICODE,json TEXT UNICODE,time TIMESTAMP DEFAULT CURRENT_TIMESTAMP);");
			historyDB.execSQL("INSERT INTO events (groupID, type, json) VALUES ("+groupID+",'"+msgType+"','"+json+"');");
		}
	}
	
	// API Error Messages
	private void showError(String shortError,String longError)
	{
		Log.e("LIM-XMPP",shortError+" [ "+longError+" ] ");
		m_lastError = shortError;
		m_lastErrorLong = longError;
	}

	private String m_lastError;
	private String m_lastErrorLong;
	public String lastError() {return m_lastError;}
	public String lastErrorLong() {return m_lastErrorLong;}
	// Event Listeners
	private transient Vector<LIM_Event_Listener> m_listeners = new Vector<LIM_Event_Listener>();
	
    public void addEventListener(LIM_Event_Listener listener) {
    	m_listeners.add(listener);
    }
    
    public void removeEventListener(LIM_Event_Listener listener) {
    	m_listeners.remove(listener);
    }
    
    public void removeAllEventListeners()
    {
    	m_listeners.clear();
    }

	// Small internally used class to de/en-code messages from master to clients
	static class LIM_JSON_Message {
		LIM_JSON_Message(String t,String b)
		{
			type = t;json_body = b;
		}
		// Type code cannot contain delimiter!
		public final static String delim = "/";
		public String type;
		public String json_body;
		public String encode()
		{
			return encode(type,json_body);
		}

		public static String encode(String type,String json_body)
		{
			return type+delim+json_body;
		}
		public static LIM_JSON_Message decode(String msg)
		{
			int delimPos = msg.indexOf(delim);
			return new LIM_JSON_Message(msg.substring(0,delimPos),msg.substring(delimPos+delim.length(),msg.length()));
		}
	}
}
